#include "rendPlatform.hpp"
#include "rendCamera.hpp"

#include <math.h>

namespace rend
{

void Camera::setupDefaultPerspective()
{
	mType = PROJECTION_PERSPECTIVE;

	mTran.identity();
	mTranI.identity();
	mInvertible = true;

	updateAttribsFromCamTransform();

	mProj.persp(-1, 1, -1, 1, 1, 50);
	mProjI.invert(mProj);

	updateAttribsFromProjTransform();
}


void Camera::setupDefaultOrthographic()
{
	mType = PROJECTION_ORTHOGRAPHIC;

	mTran.identity();
	mTranI.identity();
	mInvertible = true;

	updateAttribsFromCamTransform();

	mProj.ortho(-1, 1, -1, 1, -1, 1);
	mProjI.invert(mProj);

	updateAttribsFromProjTransform();
}


Camera::Camera()
: mTarget(0, 0, -1)
{
	setupDefaultOrthographic();
}


void Camera::updateAttribsFromCamTransform()
{
	mDirtyCam = false;

	if (!mInvertible)
		return;

	mTran.getColS(3, mPos);
	mTran.getColS(2, mForward);
	mTran.getColS(1, mUp);

	mForward.negate();
}


void Camera::updateAttribsFromProjTransform()
{
	mDirtyProj = false;

	switch (mType)
	{
	case PROJECTION_PERSPECTIVE:

		assert(mProj[3][2] == -1.f && mProj[3][3] == 0.f);

		mClipN = - mProj[2][3] / (1.f - mProj[2][2]);
		mClipF = mProj[2][3] * mClipN / (mProj[2][3] + 2.f * mClipN);

		// note: assumed a view-centered projection plane
		mProjExtX = mClipN / mProj[0][0];
		mProjExtY = mClipN / mProj[1][1];

		mFovX = ::atanf(mProjExtX / mClipN);
		mFovY = ::atanf(mProjExtY / mClipN);

		assert(	mProjExtX > 0.f &&
				mProjExtY > 0.f &&
				mClipN > 0.f &&
				mClipF > mClipN );
		break;

	case PROJECTION_ORTHOGRAPHIC:

		assert(mProj[3][2] == 0.f && mProj[3][3] == 1.f);

		mClipN = (mProj[2][3] + 1.f) / mProj[2][2];
		mClipF = mClipN - 2.f / mProj[2][2];

		// note: assumed a view-centered projection plane
		mProjExtX = 1.f / mProj[0][0];
		mProjExtY = 1.f / mProj[1][1];

		assert(	mProjExtX > 0.f &&
				mProjExtY > 0.f );
		break;

	default:
		assert(false);
	}
}


void Camera::updateCamTransformFromAttribs() const
{
	vect3 forward, right;
	forward.negate(mForward);
	right.cross(mUp, forward);

	mTran.setColS<3>(3, mPos);
	mTran.setColS<3>(2, forward);
	mTran.setColS<3>(1, mUp);
	mTran.setColS<3>(0, right);

	static const vect4 __bottom(0, 0, 0, 1);
	mTran.setRow(3, __bottom);

	mInvertible = mTranI.invert(mTran);

	assert(mInvertible);

	mDirtyCam = false;
}


void Camera::updateProjTransformFromAttribs() const
{
	switch (mType)
	{
	case PROJECTION_PERSPECTIVE:

		mProj.persp(-mProjExtX, mProjExtX, -mProjExtY, mProjExtY, mClipN, mClipF);
		break;

	case PROJECTION_ORTHOGRAPHIC:

		mProj.ortho(-mProjExtX, mProjExtX, -mProjExtY, mProjExtY, mClipN, mClipF);
		break;
	}

	mProjI.invert(mProj);
	mDirtyProj = false;
}


bool Camera::setWorldTransform(const matx4& matx)
{
	mTran = matx;
	mInvertible = mTranI.invert(matx);

	assert( mInvertible );

	updateAttribsFromCamTransform();

	return mInvertible;
}


const matx4& Camera::getWorldTransform() const
{
	if (mDirtyCam)
		updateCamTransformFromAttribs();

	return mTran;
}


bool Camera::setInverseWorldTransform(const matx4& matx)
{
	mTranI = matx;
	mInvertible = mTran.invert(matx);

	assert(mInvertible);

	updateAttribsFromCamTransform();

	return mInvertible;
}


const matx4& Camera::getInverseWorldTransform() const
{
	if (mDirtyCam)
		updateCamTransformFromAttribs();

	return mTranI;
}


void Camera::setProjection(const matx4& matx)
{
	assert((mType == PROJECTION_ORTHOGRAPHIC && matx[3][2] == 0.f && matx[3][3] == 1.f) ||
		   (mType == PROJECTION_PERSPECTIVE && matx[3][2] == -1.f && matx[3][3] == 0.f));

	mProj = matx;
	const bool invertible = mProjI.invert(matx);

	assert(invertible);

	updateAttribsFromProjTransform();
}

const matx4& Camera::getProjection() const
{
	if (mDirtyProj)
		updateProjTransformFromAttribs();

	return mProj;
}

const matx4& Camera::getInverseProjection() const
{
	if (mDirtyProj)
		updateProjTransformFromAttribs();

	return mProjI;
}

void Camera::setPosition(const vect3& position, const bool targetLock)
{
	mPos = position;

	if (targetLock)
	{
		mForward.sub(mTarget, mPos);
		mForward.normalise();
	}

	mDirtyCam = true;
}


const vect3& Camera::getPosition() const
{
	return mPos;
}


void Camera::setDirection(const vect3& forward)
{
	mForward.normalise(forward);

	mDirtyCam = true;
}


const vect3& Camera::getDirection() const
{
	return mForward;
}


void Camera::setRoll(const vect3& up)
{
	mUp.normalise(up);

	mDirtyCam = true;
}


const vect3& Camera::getRoll() const
{
	return mUp;
}


void Camera::setTarget(const vect3& target)
{
	mTarget = target;
	mForward.sub(target, mPos);
	mForward.normalise();

	mDirtyCam = true;
}


const vect3& Camera::getTarget() const
{
	return mTarget;
}


void Camera::setFovX(const float angle)
{
	assert(mType == PROJECTION_PERSPECTIVE);
	assert(angle > 0.f && angle < float(M_PI_2));
	assert(mClipN > 0.f);

	mFovX = angle;
	mProjExtX = ::tanf(mFovX) * mClipN;

	mDirtyProj = true;
}


float Camera::getFovX() const
{
	return mFovX;
}


void Camera::setFovY(const float angle)
{
	assert(mType == PROJECTION_PERSPECTIVE);
	assert(angle > 0.f && angle < float(M_PI_2));
	assert(mClipN > 0.f);

	mFovY = angle;
	mProjExtY = ::tanf(mFovY) * mClipN;

	mDirtyProj = true;
}


float Camera::getFovY() const
{
	return mFovY;
}


void Camera::setExtentX(const float extent)
{
	assert(extent > 0.f);

	mProjExtX = extent;

	if (mType == PROJECTION_PERSPECTIVE)
	{
		assert(mClipN > 0.f);

		mFovX = ::atanf(mProjExtX / mClipN);
	}

	mDirtyProj = true;
}


float Camera::getExtentX() const
{
	return mProjExtX;
}


void Camera::setExtentY(const float extent)
{
	assert(extent > 0.f);

	mProjExtY = extent;

	if (mType == PROJECTION_PERSPECTIVE)
	{
		assert(mClipN > 0.f);

		mFovY = ::atanf(mProjExtY / mClipN);
	}

	mDirtyProj = true;
}


float Camera::getExtentY() const
{
	return mProjExtY;
}


void Camera::setNearClipDistance(const float distance)
{
	mClipN = distance;

	if (mType == PROJECTION_PERSPECTIVE)
	{
		assert(mClipN > 0.f);

		mProjExtX = ::tanf(mFovX) * mClipN;
		mProjExtY = ::tanf(mFovY) * mClipN;
	}

	mDirtyProj = true;
}


float Camera::getNearClipDistance() const
{
	return mClipN;
}


void Camera::setFarClipDistance(const float distance)
{
	assert(mType != PROJECTION_PERSPECTIVE || distance > 0.f);

	mClipF = distance;
	mDirtyProj = true;
}


float Camera::getFarClipDistance() const
{
	return mClipF;
}


void Camera::setProjectionType(const Projection type)
{
	switch (type)
	{
	case PROJECTION_PERSPECTIVE:

		setupDefaultPerspective();
		break;

	case PROJECTION_ORTHOGRAPHIC:

		setupDefaultOrthographic();
		break;

	default:
		assert(false);
	}
}


Camera::Projection Camera::getProjectionType() const
{
	return mType;
}


bool Camera::getFrustum(Frustum& frustum, const matx4& origin) const
{
	if (mDirtyCam)
		updateCamTransformFromAttribs();

	if (!mInvertible)
		return false;

	if (mDirtyProj)
		updateProjTransformFromAttribs();

	matx4 clip, view;
	view.mul(mTranI, origin);
	clip.mul(mProj, view);

	frustum.setPlane(Frustum::PLANE_LEFT,
						vect4(clip[3][0] + clip[0][0],
							  clip[3][1] + clip[0][1],
							  clip[3][2] + clip[0][2],
							  clip[3][3] + clip[0][3]));

	frustum.setPlane(Frustum::PLANE_RIGHT,
						vect4(clip[3][0] - clip[0][0],
							  clip[3][1] - clip[0][1],
							  clip[3][2] - clip[0][2],
							  clip[3][3] - clip[0][3]));

	frustum.setPlane(Frustum::PLANE_BOTTOM,
						vect4(clip[3][0] + clip[1][0],
							  clip[3][1] + clip[1][1],
							  clip[3][2] + clip[1][2],
							  clip[3][3] + clip[1][3]));

	frustum.setPlane(Frustum::PLANE_TOP,
						vect4(clip[3][0] - clip[1][0],
							  clip[3][1] - clip[1][1],
							  clip[3][2] - clip[1][2],
							  clip[3][3] - clip[1][3]));

	frustum.setPlane(Frustum::PLANE_NEAR,
						vect4(clip[3][0] + clip[2][0],
							  clip[3][1] + clip[2][1],
							  clip[3][2] + clip[2][2],
							  clip[3][3] + clip[2][3]));

	frustum.setPlane(Frustum::PLANE_FAR,
						vect4(clip[3][0] - clip[2][0],
							  clip[3][1] - clip[2][1],
							  clip[3][2] - clip[2][2],
							  clip[3][3] - clip[2][3]));

	// compute a 'frustum-internal' point useful for some of the intersection tests;
	// following computation is robust but good only for orthographic cameras. keep computation and simulate orhto cam if needed
	Frustum frust_tmp, &frust = mType == PROJECTION_PERSPECTIVE ? frust_tmp : frustum;

	if (mType == PROJECTION_PERSPECTIVE)
	{
		clip.ortho(-mProjExtX, mProjExtX, -mProjExtY, mProjExtY, mClipN, mClipF);
		clip.mur(view);

		frust.setPlane(Frustum::PLANE_LEFT,
			vect4(clip[3][0] + clip[0][0],
				  clip[3][1] + clip[0][1],
				  clip[3][2] + clip[0][2],
				  clip[3][3] + clip[0][3]));

		frust.setPlane(Frustum::PLANE_RIGHT,
			vect4(clip[3][0] - clip[0][0],
				  clip[3][1] - clip[0][1],
				  clip[3][2] - clip[0][2],
				  clip[3][3] - clip[0][3]));

		frust.setPlane(Frustum::PLANE_BOTTOM,
			vect4(clip[3][0] + clip[1][0],
				  clip[3][1] + clip[1][1],
				  clip[3][2] + clip[1][2],
				  clip[3][3] + clip[1][3]));

		frust.setPlane(Frustum::PLANE_TOP,
			vect4(clip[3][0] - clip[1][0],
				  clip[3][1] - clip[1][1],
				  clip[3][2] - clip[1][2],
				  clip[3][3] - clip[1][3]));

		frust.setPlane(Frustum::PLANE_NEAR,
			vect4(clip[3][0] + clip[2][0],
				  clip[3][1] + clip[2][1],
				  clip[3][2] + clip[2][2],
				  clip[3][3] + clip[2][3]));

		frust.setPlane(Frustum::PLANE_FAR,
			vect4(clip[3][0] - clip[2][0],
				  clip[3][1] - clip[2][1],
				  clip[3][2] - clip[2][2],
				  clip[3][3] - clip[2][3]));
	}

	vect3 a, b, c;

	a = vect3::castU<4>(frust.getPlane(Frustum::PLANE_NEAR));
	a.mul(-frust.getPlane(Frustum::PLANE_NEAR)[3]);

	b = vect3::castU<4>(frust.getPlane(Frustum::PLANE_FAR));
	b.mul(-frust.getPlane(Frustum::PLANE_FAR)[3]);

	c.add(a, b);

	a = vect3::castU<4>(frust.getPlane(Frustum::PLANE_RIGHT));
	a.mul(-frust.getPlane(Frustum::PLANE_RIGHT)[3]);

	c.add(a);

	b = vect3::castU<4>(frust.getPlane(Frustum::PLANE_LEFT));
	b.mul(-frust.getPlane(Frustum::PLANE_LEFT)[3]);

	c.add(b);

	a = vect3::castU<4>(frust.getPlane(Frustum::PLANE_TOP));
	a.mul(-frust.getPlane(Frustum::PLANE_TOP)[3]);

	c.add(a);

	b = vect3::castU<4>(frust.getPlane(Frustum::PLANE_BOTTOM));
	b.mul(-frust.getPlane(Frustum::PLANE_BOTTOM)[3]);

	c.add(b);
	c.mul(.5f);

	frustum.setInternalPoint(c);

	return true;
}

} // namespace rend
